import os
import sys
import logging
import subprocess
import re
import json
import http.client
import time


LOG_LEVEL = {
    'DEBUG': logging.DEBUG,
    'INFO': logging.INFO,
    'WARNING': logging.WARNING,
    'ERROR': logging.ERROR
}

MODIFIED_STRATEGY = {
    'ONLY_ADD': (lambda a, d: a),
    'DIFFERENCE': (lambda a, d: a - d),
    'BOTH': (lambda a, d: a + d)
}

INFINIT_LIMIT = -1
INVALID_RESP = -1
DEFAULT_LIMIT = 1000
DEFAULT_LOG_LEVEL = 'INFO'
DEFAULT_STRATEGY = 'ONLY_ADD'
IS_DEBUG = False

EXIT_CODE_PASS_CHECK = 0
EXIT_CODE_FAIL_CHECK = 1
EXIT_CODE_PARAM_ERROR = 2
EXIT_CODE_NOT_GIT_PATH = 3
EXIT_CODE_INVALID_RESPONSE = 4
EXIT_CODE_GET_MODIFIED_LINE_ERROR = 5

# 配置在 参数设置 里
ENV_LINE_LIMIT = 'LINE_LIMIT'
ENV_LOG_LEVEL = 'LOG_LEVEL'
ENV_LOG_FILE = 'LOG_FILE'
ENV_ACCESS_TOKEN = 'gitcode_token'
ENV_PAYLOAD = 'webhook_payload'
ENV_MODIFY_STRATEGY = 'MODIFIED_STRATEGY'
ENV_MAX_RETRIES = 'MAX_RETRIES'


logger = None


def log_debug(log_str):
    if IS_DEBUG:
        logger.warning(log_str)


def request_modified_line_num_by_api(strategy, owner, repo, pr_id):
    access_token = os.getenv(ENV_ACCESS_TOKEN)

    max_retries = os.getenv(ENV_MAX_RETRIES)
    if max_retries is None:
        max_retries = 3

    payload = ''
    headers = {
        'Accept': 'application/json'
    }
    for attempt in range(max_retries):
        try:
            conn = http.client.HTTPSConnection("api.gitcode.com")
            logger.info(f'Req at: /api/v5/repos/{owner}/{repo}/pulls/{pr_id}/files.json')
            conn.request(
                "GET", 
                f"/api/v5/repos/{owner}/{repo}/pulls/{pr_id}/files.json?access_token={access_token}", 
                payload, 
                headers
            )
            res = conn.getresponse()
            data = res.read().decode("utf-8")
            log_debug(f">>> responce status {res.status} >>> {isinstance(res.status, int)}")

            if res.status == 200:
                log_debug(data)

                resp_json = json.loads(data)
                added_lines = int(resp_json.get('added_lines', 0))
                removed_lines = int(resp_json.get('remove_lines', 0))
                
                log_debug(f'Api: Added - {added_lines}, Deleted: -{removed_lines}')
                return MODIFIED_STRATEGY[strategy](added_lines, removed_lines)
            elif res.status == 404:
                logger.error(f"GitCode API BUG : {data}")
                logger.info(f"Attempt {attempt+1}: 404 - Retrying in {2**attempt} seconds...")
                time.sleep(2**attempt)
            else:
                logger.error(f'Invalid Response: {data}')
                return
        finally:
            conn.close()
    else:
        return None


def request_modified_line_num_by_diff(strategy, pr_id):
    try:
        diff_res = subprocess.check_output(
            ['git', 'diff', "--stat", 'HEAD~1', 'HEAD'], 
            shell=True, universal_newlines=True
        )
        diff = diff_res.splitlines()[-1]
        added, removed = list(map(int, re.findall(r"\d+", diff)[1:]))
        log_debug(f'Git diff: Added - {added}, Deleted - {removed}')
        return MODIFIED_STRATEGY[strategy](added, removed)
    except subprocess.CalledProcessError as e:
        logger.error(e)
        return None


def request_modified_line_num():
    payload = os.getenv(ENV_PAYLOAD)
    owner, repo, pr_id = parse_payload(payload)

    strategy = os.getenv(ENV_MODIFY_STRATEGY)
    if strategy is None:
        strategy = DEFAULT_STRATEGY
    log_debug(f"strategy >> {strategy}")

    api_ln = request_modified_line_num_by_api(strategy, owner, repo, pr_id)
    # git_ln = request_modified_line_num_by_diff(strategy, pr_id)

    if api_ln is not None:
        return api_ln
    
    # if git_ln is not None:
    #     return git_ln


def get_key_pattern(json_str, key_list):
    patterns = []
    for i, k in enumerate(key_list):
        if i < len(key_list) - 1:
            pat = fr"""['",\s{{]{k}['"]?\s*:\s*{{"""
        else:
            pat = fr"""['",\s{{]{k}['"]?\s*:\s*['"]?([^'",}}]+)"""
        patterns.append(pat)
    pat_str = ".*?".join(patterns)
    regex = re.compile(pat_str, re.DOTALL | re.IGNORECASE)
    match = regex.search(json_str)
    if match:
        return match.group(1).strip()
    return None
        

def parse_payload(payload):
    if not payload:
        logger.error("WEBHOOK_PAYLOAD must not be empty")
        sys.exit(EXIT_CODE_PARAM_ERROR)

    event_type = get_key_pattern(payload, ["event_type"])
    if event_type == "note":
        owner = get_key_pattern(payload, ["merge_request", "target", "namespace"])
        repo = get_key_pattern(payload, ["merge_request", "target", "name"])
        pr_number = get_key_pattern(payload, ['merge_request', 'iid'])
        return owner, repo, pr_number
    elif event_type == "merge_request":
        owner = get_key_pattern(payload, ["object_attributes", "target", "namespace"])
        repo = get_key_pattern(payload, ["object_attributes", "target", "name"])
        pr_number = get_key_pattern(payload, ['object_attributes', 'iid'])
        return owner, repo, pr_number
        
    logger.error("invalid event_type:{}".format(payload["event_type"]))
    sys.exit(EXIT_CODE_PARAM_ERROR)


def run_line_limit_check():
    logger.info('Start checking modified line limit...')

    modified_line_limit = os.getenv(ENV_LINE_LIMIT)
    try:
        modified_line_limit = int(modified_line_limit)
    except Exception:
        modified_line_limit = DEFAULT_LIMIT
    log_debug(f'Passing line limit {modified_line_limit}')
    
    logger.info('Modified Line Limit: {}'.format(modified_line_limit))

    res_code = EXIT_CODE_PASS_CHECK
    if modified_line_limit == INFINIT_LIMIT:
        logger.warning("Skip line limit check!")
        res_code = EXIT_CODE_PASS_CHECK
    else:
        ln = request_modified_line_num()
        if ln is None:
            logger.error("Can not find modified lines.")
            res_code = EXIT_CODE_GET_MODIFIED_LINE_ERROR
        else:
            logger.info(f'Modified line num: {ln}')
            if ln == INVALID_RESP or ln > modified_line_limit:
                logger.error('Line limit check FAILED!')
                res_code = EXIT_CODE_FAIL_CHECK
            else:
                logger.info('Line limit check SUCCEEDED!!')
                res_code = EXIT_CODE_PASS_CHECK
    
    logger.info('Finish modified line limit check!')

    return res_code


def setup_logging():
    log_file = os.getenv(ENV_LOG_FILE)
    log_level = os.getenv(ENV_LOG_LEVEL)

    isOutputFile = log_file is not None and log_file.strip() != ""
    if log_level not in LOG_LEVEL:
        log_level = DEFAULT_LOG_LEVEL

    global logger
    global IS_DEBUG
    IS_DEBUG = log_level == 'DEBUG'
    logger = logging.getLogger('LineLimitChecker')
    logger.setLevel(LOG_LEVEL[log_level])
    formatter = logging.Formatter('[%(name)s] - [%(levelname)s] - %(message)s')

    console_handler = logging.StreamHandler()
    console_handler.setLevel(LOG_LEVEL[log_level])
    console_handler.setFormatter(formatter)
    logger.addHandler(console_handler)
    
    if isOutputFile:
        if not os.path.exists(log_file):
            logger.warning('{} not exist!'.format(log_file))
        else:
            file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
            file_handler = logging.FileHandler(log_file)
            file_handler.setLevel(LOG_LEVEL[log_level])
            file_handler.setFormatter(file_formatter)
            logger.addHandler(file_handler)


def is_git_repo(path):
    try:
        res = subprocess.run(
            ['git', '-C', path, 'rev-parse', '--show-toplevel'], capture_output=True, text=True)
        return res.returncode == 0
    except FileNotFoundError:
        return False


if __name__ == "__main__":
    cwd = os.getcwd()
    if not is_git_repo(cwd):
        logger.error(f'{cwd} is not a git path!!')
        sys.exit(EXIT_CODE_NOT_GIT_PATH)
    setup_logging()
    sys.exit(run_line_limit_check())